// -*-Igor-*-
// ###################################################################
//  Igor Pro - JEG Tools
// 
//  FILE: "JEG FFT Between Cursors"
//                                    created: 12/17/98 {10:08:41 PM} 
//                                last update: 8/5/03 {1:51:00 PM} 
//  Author: Jonathan Guyer
//  E-mail: jguyer@his.com
//    mail: POMODORO no seisan
//     www: http://www.his.com/jguyer/
//  
// ========================================================================
//               Copyright (c) 1999-2003 Jonathan Guyer
// ========================================================================
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose and without fee is hereby granted,
// provided that the above copyright notice appear in all copies and that
// both that the copyright notice and warranty disclaimer appear in
// supporting documentation.
// 
// Jonathan Guyer disclaims all warranties with regard to this software,
// including all implied warranties of merchantability and fitness.  In
// no event shall Jonathan Guyer be liable for any special, indirect or
// consequential damages or any damages whatsoever resulting from loss of
// use, data or profits, whether in an action of contract, negligence or
// other tortuous action, arising out of or in connection with the use or
// performance of this software.
// ========================================================================
//
//  Description: 
//
//      Based loosely on WaveMetrics' <DFTMagPhase>
//      
//      Creates an automatically updating FFT magnitude (and phase) display
//      of a given trace. The FFT is calculated from the portion of the
//      trace between the cursors.
//      
//      Linear vs. dB scaling and Hann windowing can be applied dynamically,
//      as can the display of the phase wave.
//
//  History
// 
//  modified   by  rev reason
//  ---------- --- --- -----------
//  1998-12-17 JEG 0.1 original
//  1999-11-17 JEG 1.0 Radical brain surgery. Everything live now.
//  2003-08-05 JEG 1.1 Window hooks must return 0 unless they do something.
// ###################################################################
// 

#pragma rtGlobals=1

#pragma IgorVersion=4

#include "JEG Window Hooks"

Menu "Macros"
	"FFT Between Cursors", JEG_FFT_BetweenCursors()
End

// 
// -------------------------------------------------------------------------
// 
// "JEG_FFT_BetweenCursors" --
// 
//  Checks if cursors are already on the same trace. If not, prompt for
//  which trace to FFT, otherwise, gets on with it.
// 
// Results:
//  None
// 
// Side effects:
//  Everything
// -------------------------------------------------------------------------
// 
Function JEG_FFT_BetweenCursors()

	// Create state variables in this window's WinGlobals folder
	String df = GetDataFolder(1)
	NewDataFolder/O root:WinGlobals
	NewDataFolder/O/S root:WinGlobals:$(WinName(0,1))	
	
	// Can't use CsrWaveRef().
	// We need to verify the _traces_ the cursors are on, not the waves.
	String/G S_CursorAInfo, S_CursorBInfo
	// Get Igor to automagically put info into the cursor variables
	DoUpdate
	String traceAName = StringByKey("TNAME", S_CursorAInfo)
	String traceBName = StringByKey("TNAME", S_CursorBInfo)
	
	SetDataFolder df
	
	// If we've already got both cursors on the same trace, work with those, 
	// otherwise, prompt the user
	if ((strlen(traceAName) > 0) && (cmpstr(traceAName,traceBName) == 0))
		JEG_FFT_MagPhase(WinName(0,1),traceAName)
	else
		Execute "JEG_FFT_PromptAndPlot()"
	endif
End

// 
// -------------------------------------------------------------------------
// 
// "JEG_FFT_PromptAndPlot" --
// 
//  Ask user which trace to put the cursors on, then set up the FFT
// 
// Results:
//  None
// 
// Side effects:
//  Everything
// -------------------------------------------------------------------------
// 
Proc JEG_FFT_PromptAndPlot(traceName)
	String traceName
	Prompt traceName,"Input data:",popup TraceNameList("",";",1)

	Silent 1
	
	// Put the cursors on the selected wave, 
	// encompassing the entire data range
	Cursor/P A, $traceName, 0
	Cursor/P B, $traceName, numpnts(TraceNameToWaveRef("",traceName))-1
	
	JEG_FFT_MagPhase(WinName(0,1),traceName)
End

// 
// -------------------------------------------------------------------------
// 
// "JEG_FFT_MagPhase" --
// 
//   Create and plot the magnitude and phase of the specified trace of the
//   specified graph.  Henceforth, the FFT will be calculated and
//   displayed automatically.
// 
// Results:
//  None
// 
// Side effects:
//  Data folders, waves, variables, and strings created.
//  Automatic links established.
//  Plot generated.
// -------------------------------------------------------------------------
// 
Function JEG_FFT_MagPhase(graphName,traceName)
	String graphName, traceName
	
	// Since the FFT automatically follows the cursors, name the FFT
	// waves after the graph they're from, rather than a trace or wave.
	String magw = graphName + " Magnitude"
	String phzw = graphName + " Phase"
	
	// Just to establish their existence. 
	// They'll be redimensioned automatically.
	Make/O/N=1 $magw, $phzw
	Wave magWave = $magw
	Wave phzWave = $phzw
	
	// Display the magnitude wave in red
	Display/K=1 $magw
	Label left "\\K(65535,0,0) Magnitude / \\U"
	
	// Create a WinGlobals folder for the FFT state variables
	String df = GetDataFolder(1)
	NewDataFolder/O root:WinGlobals
	NewDataFolder/O/S root:WinGlobals:$graphName	

	// Store the name of the window the FFT's in for later reference
	String/G S_JEG_FFT_window = WinName(0,1)
		
	// Store the locations of these pertinant waves
	String/G S_JEG_FFT_magnitude = GetWavesDataFolder(magWave,2)
	String/G S_JEG_FFT_phase = GetWavesDataFolder(phzWave,2)
	String/G S_JEG_FFT_source = GetWavesDataFolder(TraceNameToWaveRef(graphName,traceName),2)
	
	// Flags and variables to determine the FFT appearance
	Variable/G V_JEG_FFT_Hann = 0         // Boolean. Apply Hanning window?
	Variable/G V_JEG_FFT_phase = 0        // Display phase? 1 = no, 2 = in radians, 3 = in degrees
	Variable/G V_JEG_FFT_unwrap = 1       // Boolean. If phase is displayed, unwrap it, modulo 2 Pi?
	Variable/G V_JEG_FFT_dB = 0           // Boolean. Display magnitude in dB or linear scale?
	
	// Automagic variables. Igor updates these whenever the cursors change.
	String/G S_CursorAInfo, S_CursorBInfo
	
	// Set up automatic action if the cursors are moved to a different trace	
	String/G S_JEG_FFT_trace = ""
	Variable/G V_JEG_FFT_triggerTrace
	SetFormula V_JEG_FFT_triggerTrace, "JEG_FFT_TriggerTrace(\"" + graphName + "\",S_JEG_FFT_trace)"

	// Set up automatic action if either of the cursors move, or if
	// any of the control settings are changed
	String s = "JEG_FFT_Trigger(S_CursorAInfo,S_CursorBInfo,"
	s += S_JEG_FFT_source 
	s += ",V_JEG_FFT_dB,V_JEG_FFT_Hann,V_JEG_FFT_phase,V_JEG_FFT_unwrap)"
	Variable/G V_JEG_FFT_trigger
	SetFormula V_JEG_FFT_trigger, s
	
	// Set up automatic action if the phase popup is changed
	Variable/G V_JEG_FFT_triggerPhase
	SetFormula V_JEG_FFT_triggerPhase, "JEG_FFT_TriggerPhase(\"" + graphName + "\",V_JEG_FFT_phase)"

	// Turn off phase display initially
	V_JEG_FFT_phase = 1
	DoUpdate
	
	// Create controls in the FFT window for live adjustment of its display
	
	// Linear or dB magnitude?
	CheckBox $("dB_" + graphName), noproc, variable=V_JEG_FFT_dB
	CheckBox $("dB_" + graphName), pos={0,2}, size={30,14}, title="dB"
	
	// Apply Hanning window?
	CheckBox $("Hanning_" + graphName), noproc, variable = V_JEG_FFT_Hann
	CheckBox $("Hanning_" + graphName), pos={40,2}, size={50,14}, title="Hanning"
	
	// Display phase? If so, in degrees or radians?
	PopupMenu $("Phase_" + graphName), mode = V_JEG_FFT_phase, pos={120,1}, size={100,14}
	PopupMenu $("Phase_" + graphName), proc=JEG_FFT_PhaseMenu, title="Phase"
	PopupMenu $("Phase_" + graphName), value="none;in radians;in degrees"
	
	// If displayed, unwrap the phase (normally modulo 2 Pi)?
	CheckBox $("Wrap_" + graphName), noproc, variable = V_JEG_FFT_unwrap, pos={265,2}
	CheckBox $("Wrap_" + graphName), size={110,14}, title="Unwrap phase", value=V_JEG_FFT_unwrap
		
	ControlBar 23	
	
	SetDataFolder df
	
	// Bring the data graph back in front of the FFT graph 
	// to make life easy on the user
	DoWindow/F $graphName	
	
	// Ensure everything gets cleaned up when the FFT source window is closed.
	JEG_WindowHook_Set(graphName,"JEG_FFT_Hook")
End	
	
// 
// -------------------------------------------------------------------------
// 
// "JEG_FFT_Trigger" --
// 
//  Called whenever the cursors are moved in the source window, or when
//  any of the FFT display parameters is changed.
// 
// Results:
//  None
// 
// Side effects:
//  FFT magnitude and phase are recalculated
// -------------------------------------------------------------------------
// 
Function JEG_FFT_Trigger(cursorAInfo,cursorBInfo,w,dB,window,phase,unwrapPhase)
	String cursorAInfo, cursorBInfo
	Wave   w
	Variable dB,window, phase, unwrapPhase

	// Extract pertinant information from the automagic cursor variables
	String graphAName = StringByKey("GRAPH", cursorAInfo)	
	String traceAName = StringByKey("TNAME", cursorAInfo)
	String cursorAName = StringByKey("CURSOR", cursorAInfo)
	Variable xPositionA = NumberByKey("POINT", cursorAInfo)

	String graphBName = StringByKey("GRAPH", cursorBInfo)	
	String traceBName = StringByKey("TNAME", cursorBInfo)
	String cursorBName = StringByKey("CURSOR", cursorBInfo)
	Variable xPositionB = NumberByKey("POINT", cursorBInfo)
	
	// Move to the appropriate WinGlobal data folder
	String df = GetDataFolder(1)
	NewDataFolder/O root:WinGlobals
	NewDataFolder/O/S root:WinGlobals:$graphAName	
	
	SVAR magPath = S_JEG_FFT_magnitude
	SVAR phzPath = S_JEG_FFT_phase
	
	SVAR FFTtrace = S_JEG_FFT_trace
	
	Wave/C magW = $magPath
	Wave phzW = $phzPath
	
	// If the cursors are not on the same trace (in the same graph!?!)
	// then NaN the FFT wave, because it makes no sense
	if ((cmpstr(traceAName,traceBName) != 0) || (cmpstr(graphAName,graphBname) != 0))
		// I don't see how the graphs could ever be different, but who knows...
		magW = NaN
		phzW = NaN
		FFTtrace = ""
		SetDataFolder df
		return 0
	endif
	
	// Keep the trace name up to date (generates another auto-trigger)
	if (cmpstr(traceAName,FFTtrace) != 0)
		FFTtrace = traceAName
	endif
	
	Wave sourceW = TraceNameToWaveRef(graphAName,traceAName)
	SVAR sourcePath = S_JEG_FFT_source
	
	// If the cursors move to a new trace, we need to regenerate the
	// trigger. We can't have the trigger depend on a variable containing 
	// the path to the source wave, because the FFT wouldn't update
	// when the contents of the source wave changed.
	if (cmpstr(sourcePath,GetWavesDataFolder(sourceW,2)) != 0)
		sourcePath = GetWavesDataFolder(sourceW,2)
		String s = "JEG_FFT_Trigger(S_CursorAInfo,S_CursorBInfo,"
		s += sourcePath 
		s += ",V_JEG_FFT_dB,V_JEG_FFT_Hann,V_JEG_FFT_phase,V_JEG_FFT_unwrap)"
		Variable/G V_JEG_FFT_trigger
		SetFormula V_JEG_FFT_trigger, s
	endif
	
	// Put the cursor positions in the right order
	Variable minX, maxX
	if (xPositionA > xPositionB)
		minX = xPositionB
		maxX = xPositionA
	else
		minX = xPositionA
		maxX = xPositionB
	endif
	
	// Adjust the cursor positions so that we transform an even number 
	// of points, as required by the DFT. This algorithm is a little
	// asymmetric:
	//
	//   If you move the "lower" cursor, then the FFT will be applied
	//   to [A,B], [A+1,B+1], [A+2,B], [A+3,B+1], ...
	//
	//   If you move the "upper" cursor, then the FFT will be applied
	//   to [A,B], [A,B+2], [A,B+2], [A,B+4], [A,B+4], ...
	//
	// Without getting more sophisticated about which cursor is moving,
	// this can't be avoided. Since this is all just a bit of subterfuge
	// to keep a reasonably valid FFT displayed the whole time the
	// cursor is moving, it hardly seems worth the trouble.
	if (mod(maxX - minX + 1,2) != 0)
		if (maxX < numpnts(sourceW) - 1)
			// if possible, nudge the high boundary out a little
			maxX += 1
		else
			// otherwise, if possible nudge the high boundary in a little
			if (maxX > minX + 1)
				maxX -= 1
			else
				// otherwise, nudge the low boundary out a little
				if (minX > 0)
					minX -= 1
				else
					// give up
					magW = NaN
					phzW = NaN
					SetDataFolder df
					return 0					
				endif
			endif
		endif
	endif
	
	// According to the docs, FFT should work on a 2 point wave 
	// (even though the result is sorta stupid), but it doesn't.
	if (maxX < minX + 3)
		magW = NaN
		phzW = NaN
		SetDataFolder df
		return 0					
	endif	

	// Copy the section of the trace between the cursors
	Duplicate/C/O/R=[minX,maxX] sourceW, $magPath
	Wave xWave = CsrXWaveRef(A, graphAName)
	if ( WaveExists(xWave))
		// This isn't right. We should interpolate, not rescale.
		SetScale/I x, xWave[minX], xWave[maxX], $magPath
	endif

	if (window)
		Hanning magW
		magW *= 2		// *= 2 assumes continuous rather than pulsed data
	endif

	FFT magW
	
	// Convert complex result into more useful magnitude and phase
	magW = r2polar(magW)
	
	if ( phase != 1 )
		// Display phase
		Duplicate/O magW $phzPath
		Redimension/R phzW
		phzW = imag(magW)
		if ( unwrapPhase )
			// By default, phase is modulo 2 Pi
			phzW[0] = phzW[1]			// try to avoid glitch at dc
			UnWrap 2*Pi,phzW
			phzW[0]= 0
		endif
		
		if (phase==3)
			// Phase in degrees
			phzW *= 180/Pi
			SetScale y,0,0,"deg",phzW
		else
			// Phase in degrees
			SetScale y,0,0,"rad",phzW
		endif
	endif
	
	// We want real waves for display
	Redimension/R magW
	
	// Adjust the magnitude to be consistent 
	// with the continuous Fourier transform
	magW /= (maxX - minX)/2
	
	if (dB)
		// Display magnitude as dB below maximum
		WaveStats/Q magW
		Wave realMagW = magW
		realMagW = 20*log(realMagW/V_max)
		SetScale y,0,0,"dB",magW
	endif
	
	SetDataFolder df
end

// 
// -------------------------------------------------------------------------
// 
// "JEG_FFT_TriggerPhase" --
// 
//  Called whenever the phase popup is changed
// 
// Results:
//  None
// 
// Side effects:
//  FFT phase is displayed or removed and scaling is changed between
//  degrees and radians.
// -------------------------------------------------------------------------
// 
Function JEG_FFT_TriggerPhase(graphName,phase)
	String graphName
	Variable phase
	
	String df = GetDataFolder(1)
	NewDataFolder/O root:WinGlobals
	NewDataFolder/O/S root:WinGlobals:$graphName
	
	if (exists("S_JEG_FFT_window") != 2)
		// We've been triggered for a graph that doesn't appear
		// to be displaying an FFT, so abort
		return 0
	endif

	SVAR FFTwin = S_JEG_FFT_window
		
	// The FFT window should already be in front for this function
	// to be called, but just make sure
	
	// Store the current topmost graph
	String win = WinName(0,1)
	
	// Pop the FFT window to the front (if necessary)
	DoWindow/F $FFTwin
	
	SVAR phaseWavePath = S_JEG_FFT_phase
	Wave phaseWave = $phaseWavePath
	CheckDisplayed phaseWave
	
	if (phase == 1)
		// User wants no phase displayed
		if (V_flag)
			// Wave is displayed, so remove it
			RemoveFromGraph $(NameOfWave(phaseWave))
		endif	
	else	
		// Display phase
		if (!V_flag)
			// Wave is not displayed, so show it in blue
			AppendToGraph/R/C=(0,0,65535) phaseWave
			Label right "\\K(0,0,65535) Phase / \\U"
		endif	
		if (phase == 2)
			// in radians
			ModifyGraph manTick(right)={0,Pi/2,0,2},manMinor(right)={1,1}
		else
			// in degrees
			ModifyGraph manTick(right)={0,90,0,0},manMinor(right)={1,1}
		endif
	endif

	// Restore the topmost window
	DoWindow/F $win

	SetDataFolder df	
End

// 
// -------------------------------------------------------------------------
// 
// "JEG_FFT_TriggerTrace" --
// 
//  Called whenever the cursors are moved to a new trace
// 
// Results:
//  None
// 
// Side effects:
//  FFT graph title is updated to reflect which trace from which 
//  graph is the source
// -------------------------------------------------------------------------
// 
Function JEG_FFT_TriggerTrace(graphName,traceName)
	String graphName, traceName
	
	String df = GetDataFolder(1)
	SetDataFolder root:WinGlobals:$graphName
	
	SVAR FFTwindow = S_JEG_FFT_window
	String s
	
	if (strlen(traceName) > 0)
		sprintf s, "FFT of '%s' in '%s'", traceName, graphName
	else
		sprintf s, "FFT of '%s'", graphName	
	endif
	
	DoWindow/T $FFTwindow, s

	SetDataFolder df	
End

// 
// -------------------------------------------------------------------------
// 
// "JEG_FFT_PhaseMenu" --
// 
//  Called whenever the phase popup is changed.
//  Needed because popups can't be automatically linked to variables.
// 
// Results:
//  None
// 
// Side effects:
//  Phase state variable is changed
// -------------------------------------------------------------------------
// 
Function JEG_FFT_PhaseMenu(ctrlName,popNum,popStr) : PopupMenuControl
	String ctrlName
	Variable popNum		// which item is currently selected (1-based)
	String popStr		// contents of current popup item as string
	
	String graphName = StringByKey("Phase",ctrlName,"_")
	
	// Move to the correct WinGlobals folder for this graph
	String df = GetDataFolder(1)
	NewDataFolder/O root:WinGlobals
	NewDataFolder/O/S root:WinGlobals:$graphName
	
	Variable/G V_JEG_FFT_phase = popNum

	SetDataFolder df	
End

// 
// -------------------------------------------------------------------------
// 
// "JEG_FFT_Hook" --
// 
//  Take automatic action when an FFT source window is closed
// 
// Results:
//  None
// 
// Side effects:
//  Close the FFT display and kill state variables that link this graph 
//  to the FFT display
// -------------------------------------------------------------------------
// 
Function JEG_FFT_Hook(infoStr)
	String infoStr
	
	String window = StringByKey("WINDOW", infoStr)
	String event  = StringByKey("EVENT", infoStr)
	
	if (cmpstr(event, "kill") == 0)
		String df= GetDataFolder(1)
		NewDataFolder/O root:WinGlobals
		NewDataFolder/O/S root:WinGlobals:$window
		
		KillVariables/Z V_JEG_FFT_Hann, V_JEG_FFT_phase
		KillVariables/Z V_JEG_FFT_unwrap, V_JEG_FFT_dB
		KillVariables/Z V_JEG_FFT_trigger, V_JEG_FFT_triggerTrace
		KillVariables/Z V_JEG_FFT_triggerPhase
		
		KillStrings/Z S_JEG_FFT_magnitude, S_JEG_FFT_phase
		KillStrings/Z S_JEG_FFT_source, S_JEG_FFT_trace
		
		SVAR FFTWindow = S_JEG_FFT_window
		DoWindow/K $FFTWindow
		KillStrings/Z S_JEG_FFT_window
		
		SetDataFolder df
		
		return 1
	endif
	
	return 0
End
